/*
 * Copyright 2013-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.alibaba.cloud.nacos.configdata;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.huaweicloud.nacos.config.manager.NacosConfigServiceMasterManager;
import com.huaweicloud.nacos.config.manager.NacosConfigServiceStandbyManager;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;

import org.springframework.boot.BootstrapRegistry.InstanceSupplier;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.context.config.ConfigDataLocation;
import org.springframework.boot.context.config.ConfigDataLocationNotFoundException;
import org.springframework.boot.context.config.ConfigDataLocationResolver;
import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
import org.springframework.boot.context.config.ConfigDataResource;
import org.springframework.boot.context.config.ConfigDataResourceNotFoundException;
import org.springframework.boot.context.config.Profiles;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.core.Ordered;

import static com.alibaba.cloud.nacos.configdata.NacosConfigDataResource.NacosItemConfig;

/**
 * Forked from com.alibaba.cloud.nacos.configdata.NacosConfigDataLocationResolver.java
 *
 * Implementation of {@link ConfigDataLocationResolver}, load Nacos
 * {@link ConfigDataResource}.
 *
 * @author freeman
 * @since 2021.0.1.0
 */
public class NacosConfigDataLocationResolver
    implements ConfigDataLocationResolver<NacosConfigDataResource>, Ordered {
  /**
   * Prefix for Config Server imports.
   */
  public static final String PREFIX = "nacos:";

  private final Log log;

  // support params

  private static final String GROUP = "group";

  private static final String REFRESH_ENABLED = "refreshEnabled";

  private static final String PREFERENCE = "preference";

  public NacosConfigDataLocationResolver(DeferredLogFactory logFactory) {
    this.log = logFactory.getLog(getClass());
  }

  @Override
  public int getOrder() {
    return -1;
  }

  protected NacosConfigProperties loadProperties(
      ConfigDataLocationResolverContext context) {
    Binder binder = context.getBinder();
    BindHandler bindHandler = getBindHandler(context);

    NacosConfigProperties nacosConfigProperties;
    if (context.getBootstrapContext().isRegistered(NacosConfigProperties.class)) {
      nacosConfigProperties = context.getBootstrapContext()
          .get(NacosConfigProperties.class);
    } else {
      nacosConfigProperties = binder
          .bind("spring.cloud.nacos", Bindable.of(NacosConfigProperties.class),
              bindHandler)
          .map(properties -> binder
              .bind(NacosConfigProperties.PREFIX,
                  Bindable.ofInstance(properties), bindHandler)
              .orElse(properties))
          .orElseGet(() -> binder
              .bind(NacosConfigProperties.PREFIX,
                  Bindable.of(NacosConfigProperties.class), bindHandler)
              .orElseGet(NacosConfigProperties::new));
    }

    return nacosConfigProperties;
  }

  private BindHandler getBindHandler(ConfigDataLocationResolverContext context) {
    return context.getBootstrapContext().getOrElse(BindHandler.class, null);
  }

  protected Log getLog() {
    return this.log;
  }

  @Override
  public boolean isResolvable(ConfigDataLocationResolverContext context,
      ConfigDataLocation location) {
    if (!location.hasPrefix(getPrefix())) {
      return false;
    }
    return context.getBinder()
        .bind(NacosConfigProperties.PREFIX + ".enabled", Boolean.class)
        .orElse(true);
  }

  protected String getPrefix() {
    return PREFIX;
  }

  @Override
  public List<NacosConfigDataResource> resolve(
      ConfigDataLocationResolverContext context, ConfigDataLocation location)
      throws ConfigDataLocationNotFoundException,
      ConfigDataResourceNotFoundException {
    return Collections.emptyList();
  }

  @Override
  public List<NacosConfigDataResource> resolveProfileSpecific(
      ConfigDataLocationResolverContext resolverContext,
      ConfigDataLocation location, Profiles profiles)
      throws ConfigDataLocationNotFoundException {
    NacosConfigProperties properties = loadProperties(resolverContext);

    ConfigurableBootstrapContext bootstrapContext = resolverContext
        .getBootstrapContext();

    bootstrapContext.registerIfAbsent(NacosConfigProperties.class,
        InstanceSupplier.of(properties));

    registerConfigManager(properties, bootstrapContext);

    return loadConfigDataResources(location, profiles, properties);
  }

  private List<NacosConfigDataResource> loadConfigDataResources(
      ConfigDataLocation location, Profiles profiles,
      NacosConfigProperties properties) {
    List<NacosConfigDataResource> result = new ArrayList<>();
    URI uri = getUri(location, properties);

    if (StringUtils.isBlank(dataIdFor(uri))) {
      throw new IllegalArgumentException("dataId must be specified");
    }

    NacosConfigDataResource resource = new NacosConfigDataResource(properties,
        location.isOptional(), profiles, log,
        new NacosItemConfig().setGroup(groupFor(uri, properties))
            .setDataId(dataIdFor(uri)).setSuffix(suffixFor(uri, properties))
            .setRefreshEnabled(refreshEnabledFor(uri, properties))
            .setPreference(preferenceFor(uri)));
    result.add(resource);

    return result;
  }

  private String preferenceFor(URI uri) {
    return getQueryMap(uri).get(PREFERENCE);
  }

  private URI getUri(ConfigDataLocation location, NacosConfigProperties properties) {
    String path = location.getNonPrefixedValue(getPrefix());
    if (StringUtils.isBlank(path)) {
      path = "/";
    }
    if (!path.startsWith("/")) {
      path = "/" + path;
    }
    String uri = properties.getServerAddr() + path;
    return getUri(uri);
  }

  private void registerConfigManager(NacosConfigProperties properties,
      ConfigurableBootstrapContext bootstrapContext) {
    if (!bootstrapContext.isRegistered(NacosConfigServiceMasterManager.class)) {
      bootstrapContext.register(NacosConfigServiceMasterManager.class,
          InstanceSupplier.of(new NacosConfigServiceMasterManager(properties)));
    }
    if (!bootstrapContext.isRegistered(NacosConfigServiceStandbyManager.class)) {
      bootstrapContext.register(NacosConfigServiceStandbyManager.class,
          InstanceSupplier.of(new NacosConfigServiceStandbyManager(properties)));
    }
  }

  private URI getUri(String uris) {
    String realUri = uris;
    if (!realUri.startsWith("http://") && !realUri.startsWith("https://")) {
      realUri = "http://" + realUri;
    }
    URI uri;
    try {
      uri = new URI(realUri);
    } catch (URISyntaxException e) {
      throw new IllegalArgumentException("illegal URI: " + realUri, e);
    }
    return uri;
  }

  private String groupFor(URI uri, NacosConfigProperties properties) {
    Map<String, String> queryMap = getQueryMap(uri);
    return queryMap.containsKey(GROUP) ? queryMap.get(GROUP) : properties.getGroup();
  }

  private Map<String, String> getQueryMap(URI uri) {
    String query = uri.getQuery();
    if (StringUtils.isBlank(query)) {
      return Collections.emptyMap();
    }
    Map<String, String> result = new HashMap<>(4);
    for (String entry : query.split("&")) {
      String[] kv = entry.split("=");
      if (kv.length == 2) {
        result.put(kv[0], kv[1]);
      }
    }
    return result;
  }

  private String suffixFor(URI uri, NacosConfigProperties properties) {
    String dataId = dataIdFor(uri);
    if (dataId != null && dataId.contains(".")) {
      return dataId.substring(dataId.lastIndexOf('.') + 1);
    }
    return properties.getFileExtension();
  }

  private boolean refreshEnabledFor(URI uri, NacosConfigProperties properties) {
    Map<String, String> queryMap = getQueryMap(uri);
    return queryMap.containsKey(REFRESH_ENABLED)
        ? Boolean.parseBoolean(queryMap.get(REFRESH_ENABLED))
        : properties.isRefreshEnabled();
  }

  private String dataIdFor(URI uri) {
    String path = uri.getPath();
    // notice '/'
    if (path == null || path.length() <= 1) {
      return StringUtils.EMPTY;
    }
    String[] parts = path.substring(1).split("/");
    if (parts.length != 1) {
      throw new IllegalArgumentException("illegal dataId");
    }
    return parts[0];
  }
}
